const path = require('path');
const Source = require('../Source');
const puppeteer = require('../../..');
const PROJECT_DIR = path.join(__dirname, '..', '..', '..');
const fs = require('fs');
const objectDefinitions = [];
(async function() {
  const browser = await puppeteer.launch();
  const page = (await browser.pages())[0];
  const api = await Source.readFile(path.join(PROJECT_DIR, 'docs', 'api.md'));
  const {documentation} = await require('../check_public_api/MDBuilder')(page, [api]);
  await browser.close();
  const classes = documentation.classesArray.slice(1);
  const root = documentation.classesArray[0];
  const output = `// This file is generated by ${__filename.substring(path.join(__dirname, '..', '..').length)}
import { ChildProcess } from 'child_process';
import { EventEmitter } from 'events';
/**
 * Can be converted to JSON
 */
interface Serializable {}
interface ConnectionTransport {}

${root.methodsArray.map(method => `
${memberJSDOC(method, '')}export function ${method.name}${argsFromMember(method)} : ${typeToString(method.type, method.name)};
`).join('')}
${root.propertiesArray.map(property => `
${memberJSDOC(property, '')}export const ${property.name}${argsFromMember(property)} : ${typeToString(property.type, property.name)};
`).join('')}
${classes.map(classDesc => classToString(classDesc)).join('\n')}
${objectDefinitionsToString()}
`;
  fs.writeFileSync(path.join(PROJECT_DIR, 'index.d.ts'), output, 'utf8');
})();

function objectDefinitionsToString() {
  let definition;
  const parts = [];
  while ((definition = objectDefinitions.pop())) {
    const {name, properties} = definition;
    parts.push(`interface ${name} {`);
    parts.push(properties.map(member => `  ${memberJSDOC(member, '  ')}${nameForProperty(member)}${argsFromMember(member, name)}: ${typeToString(member.type, name, member.name)};`).join('\n\n'));
    parts.push('}\n');
  }
  return parts.join('\n');
}

function nameForProperty(member) {
  return (member.required || member.name.startsWith('...')) ? member.name : member.name + '?';
}

/**
 * @param {import('./check_public_api/Documentation').Class} classDesc
 */
function classToString(classDesc) {
  const parts = [];
  if (classDesc.comment) {
    parts.push(`/**
 * ${classDesc.comment.split('\n').join('\n * ')}
 */`);
  }
  parts.push(`export interface ${classDesc.name} ${classDesc.extends ? `extends ${classDesc.extends} ` : ''}{`);
  for (const method of ['on', 'once', 'addListener']) {
    for (const [eventName, value] of classDesc.events) {
      if (value.comment) {
        parts.push('  /**');
        parts.push(...value.comment.split('\n').map(line => '   * ' + line));
        parts.push('   */');
      }
      parts.push(`  ${method}(event: '${eventName}', listener: (arg0 : ${typeToString(value && value.type, classDesc.name, eventName, 'payload')}) => void): this;\n`);
    }
  }
  const members = classDesc.membersArray.filter(member => member.kind !== 'event');
  parts.push(members.map(member => `  ${memberJSDOC(member, '  ')}${member.name}${argsFromMember(member, classDesc.name)}: ${typeToString(member.type, classDesc.name, member.name)};`).join('\n\n'));
  parts.push('}\n');
  return parts.join('\n');
}

/**
 * @param {import('./check_public_api/Documentation').Type} type
 */
function typeToString(type, ...namespace) {
  if (!type)
    return 'void';
  let typeString = stringifyType(parseType(type.name));
  for (let i = 0; i < type.properties.length; i++)
    typeString = typeString.replace('arg' + i, type.properties[i].name);
  if (type.properties.length && typeString.indexOf('Object') !== -1) {
    const name = namespace.map(n => n[0].toUpperCase() + n.substring(1)).join('');
    typeString = typeString.replace('Object', name);
    objectDefinitions.push({name, properties: type.properties});
  }
  return typeString;
}

/**
 * @param {string} type
 */
function parseType(type) {
  type = type.trim();
  if (type.startsWith('?')) {
    const parsed = parseType(type.substring(1));
    parsed.nullable = true;
    return parsed;
  }
  if (type.startsWith('...'))
    return parseType('Array<' + type.substring(3) + '>');
  let name = type;
  let next = null;
  let template = null;
  let args = null;
  let retType = null;
  let firstTypeLength = type.length;
  for (let i = 0; i < type.length; i++) {
    if (type[i] === '<') {
      name = type.substring(0, i);
      const matching = matchingBracket(type.substring(i), '<', '>');
      template = parseType(type.substring(i + 1, i + matching - 1));
      firstTypeLength = i + matching;
      break;
    }
    if (type[i] === '(') {
      name = type.substring(0, i);
      const matching = matchingBracket(type.substring(i), '(', ')');
      args = parseType(type.substring(i + 1, i + matching - 1));
      i = i + matching;
      if (type[i] === ':') {
        retType = parseType(type.substring(i + 1));
        next = retType.next;
        retType.next = null;
        break;
      }
    }
    if (type[i] === '|' || type[i] === ',') {
      name = type.substring(0, i);
      firstTypeLength = i;
      break;
    }
  }
  let pipe = null;
  if (type[firstTypeLength] === '|')
    pipe = parseType(type.substring(firstTypeLength + 1));
  else if (type[firstTypeLength] === ',')
    next = parseType(type.substring(firstTypeLength + 1));
  if (name === 'Promise' && !template)
    template = parseType('void');
  return {
    name,
    args,
    retType,
    template,
    pipe,
    next
  };
}

function stringifyType(parsedType) {
  if (!parsedType)
    return 'void';
  let out = parsedType.name;
  if (parsedType.args) {
    let args = parsedType.args;
    const stringArgs = [];
    while (args) {
      const arg = args;
      args = args.next;
      arg.next = null;
      stringArgs.push(stringifyType(arg));
    }
    out = `(${stringArgs.map((type, index) => `arg${index} : ${type}`).join(', ')}, ...args: any[]) => ${stringifyType(parsedType.retType)}`;
  } else if (parsedType.name === 'function') {
    out = 'Function';
  }
  if (parsedType.nullable)
    out = 'null|' + out;
  if (parsedType.template)
    out += '<' + stringifyType(parsedType.template) + '>';
  if (parsedType.pipe)
    out += '|' + stringifyType(parsedType.pipe);
  if (parsedType.next)
    out += ', ' + stringifyType(parsedType.next);
  return out.trim();
}

function matchingBracket(str, open, close) {
  let count = 1;
  let i = 1;
  for (; i < str.length && count; i++) {
    if (str[i] === open)
      count++;
    else if (str[i] === close)
      count--;
  }
  return i;
}

/**
 * @param {import('./check_public_api/Documentation').Member} member
 */
function argsFromMember(member, ...namespace) {
  if (member.kind === 'property')
    return '';
  return '(' + member.argsArray.map(arg => `${nameForProperty(arg)}: ${typeToString(arg.type, ...namespace, member.name, 'options')}`).join(', ') + ')';
}
/**
 * @param {import('./check_public_api/Documentation').Member} member
 */
function memberJSDOC(member, indent) {
  const lines = [];
  if (member.comment)
    lines.push(...member.comment.split('\n'));
  lines.push(...member.argsArray.map(arg => `@param ${arg.name.replace(/\./g, '')} ${arg.comment.replace('\n', ' ')}`));
  if (member.returnComment)
    lines.push(`@returns ${member.returnComment}`);
  if (!lines.length)
    return '';
  return `/**
${indent} * ${lines.join('\n' + indent + ' * ')}
${indent} */
${indent}`;
}
